/*
 * Copyright (C) 2008-2009 the original author or authors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *         http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.liquidform.internal;

import static com.google.code.liquidform.internal.BeanUtils.*;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;

import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import com.google.code.liquidform.internal.managedobjects.FrameworkManagedObject;

public class PropertyAccessRecordingInvocationHandler implements
		MethodInterceptor {

	private final IProxyFactory proxyFactory = CglibProxyFactory.INSTANCE;

	private final FrameworkObjectFactory factory = FrameworkObjectFactory.INSTANCE;

	@SuppressWarnings("unchecked")
	public Object intercept(Object proxy, Method method, Object[] args,
			MethodProxy methodProxy) throws Throwable {

		if (!BeanUtils.isGetter(method)) {
			// Don't intercept system methods esp. finalize...
			// TODO : use CGLIB CallbackFilter
			return methodProxy.invokeSuper(proxy, args);
		}

		String ptyName = BeanUtils.propertyNameForGetter(method);
		FrameworkManagedObject about = AliasFactory.about(proxy);

		String fullyQualifiedPtyName = about.getPreferredStringRepresentation()
				+ "." + ptyName;

		Class ptyClass = associationOrComponentType(method);
		Class collElement = collectionElementType(method);

		Object ret = null;

		if (ptyClass == null) {
			// terminal pty or persistent collection :
			// return "dumb" instance
			ret = factory.makeNew(method.getReturnType());
		} else {
			// single valued association path :
			// return another proxy
			ret = proxyFactory.createProxy(proxy.getClass().getClassLoader(),
					this, ptyClass);
		}

		if (collElement != null) {
			// Create a proxy of type collElement and put it somewhere
			// so that LF.as() can return it
			Object elementProxy = proxyFactory.createProxy(proxy.getClass()
					.getClassLoader(), this, collElement);
			AliasFactory.rememberAsCollectionAssociationPath(ret,
					fullyQualifiedPtyName, collElement, elementProxy);
		} else {
			AliasFactory.rememberAsAssociationPath(ret, fullyQualifiedPtyName);
		}
		return ret;
	}

	/**
	 * Looks at a methods' return type and decides whether it is a collection
	 * valued getter. If so, it returns the collection element type. If not, it
	 * returns <tt>null</tt>.
	 */
	private Class<?> collectionElementType(Method method) {
		if (!Collection.class.isAssignableFrom(method.getReturnType())) {
			return null;
		}

		OneToMany oneTM = getPropertyOrGetterAnnotation(method, OneToMany.class);
		ManyToMany manyTM = getPropertyOrGetterAnnotation(method,
				ManyToMany.class);

		if (oneTM == null && manyTM == null) {
			throw new IllegalStateException(
					"Not a persistent collection (does not bear OneToMany/ManyToMany) : "
							+ method);
		}

		// Honor XXXToMany.targetEntity()
		if (oneTM != null && !void.class.equals(oneTM.targetEntity())) {
			return oneTM.targetEntity();
		}
		if (manyTM != null && !void.class.equals(manyTM.targetEntity())) {
			return manyTM.targetEntity();
		}

		// Fallback to collection generic type
		ParameterizedType pt = (ParameterizedType) method
				.getGenericReturnType();
		return (Class<?>) pt.getActualTypeArguments()[0];
	}

	/**
	 * Looks at the methods' return type annotations and decides whether it is
	 * an association or component getter. If so, it returns the actual type of
	 * the annotation to mimic. If not, this method returns <tt>null</tt>.
	 */
	private Class<?> associationOrComponentType(Method method) {
		Class<?> returnType = method.getReturnType();
		if (returnType.isAnnotationPresent(Entity.class)
				|| returnType.isAnnotationPresent(Embeddable.class)) {
			return returnType;
		}
		// If returnType is eg an interface, then the method MUST have been
		// annotated with XXXToOne
		if (getPropertyOrGetterAnnotation(method, OneToOne.class) != null
				|| getPropertyOrGetterAnnotation(method, ManyToOne.class) != null) {
			return returnType;
		}
		return null;
	}

}
